/*
 * Copyright 2014-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.rtf.framework.web.mvc.config.session.source;

import com.rtf.framework.common.config.AppProperties;
import com.rtf.framework.common.config.AppSecureProperties;
import com.rtf.framework.common.util.UtilAESCrypto;
import com.rtf.framework.common.util.UtilCollection;
import com.rtf.framework.web.common.interceptor.support.AppServletContextHolder;
import com.rtf.framework.web.mvc.config.session.AppSessionConstants;
import com.rtf.framework.web.mvc.config.session.AppSessionLoginFilter;
import com.rtf.framework.web.mvc.config.session.source.springInterface.AppSessionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.session.Session;
import org.springframework.session.web.http.*;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Switches the {@link HttpSession} implementation to be backed by a
 * {@link Session}.
 *
 * The {@link SessionRepositoryFilter} wraps the
 * {@link HttpServletRequest} and overrides the methods to get an
 * {@link HttpSession} to be backed by a
 * {@link Session} returned by the
 * {@link AppSessionRepository}.
 *
 * The {@link SessionRepositoryFilter} uses a {@link HttpSessionIdResolver} (default
 * {@link CookieHttpSessionIdResolver} to bridge logic between an
 * {@link HttpSession} and the
 * {@link Session} abstraction. Specifically:
 *
 * <ul>
 * <li>The session id is looked up using
 * {@link HttpSessionIdResolver#resolveSessionIds(HttpServletRequest)}
 * . The default is to look in a cookie named SESSION.</li>
 * <li>The session id of newly created {@link Session} is sent
 * to the client using
 * <li>The client is notified that the session id is no longer valid with
 * {@link HttpSessionIdResolver#expireSession(HttpServletRequest, HttpServletResponse)}
 * </li>
 * </ul>
 *
 * <p>
 * The SessionRepositoryFilter must be placed before any Filter that access the
 * HttpSession or that might commit the response to ensure the session is overridden and
 * persisted properly.
 * </p>
 *
 * @param <S> the {@link Session} type.
 * @since 1.0
 * @author Rob Winch
 * @author Vedran Pavic
 */
@Order(AppSessionRepositoryFilter.DEFAULT_ORDER)
public class AppSessionRepositoryFilter<S extends Session> extends SessionRepositoryFilter {

    @Autowired
    protected AppProperties appProperties;

    @Autowired
    private AppSecureProperties appSecureProperties;

    @Resource
    protected StringRedisTemplate stringRedisTemplate;


    private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR
            + ".CURRENT_SESSION";


    private final AppSessionRepository<S> sessionRepository;

    private ServletContext servletContext;
    private HttpSessionIdResolver httpSessionIdResolver = new CookieHttpSessionIdResolver();

    private boolean enableAnonSession;

    private List<String> specialAnonUrl ;

    private List<String> anonUrlList;

    public void setAnonUrlList(List<String> anonUrlList) {
        this.anonUrlList = anonUrlList;
    }

    public void setSpecialAnonUrl(List<String> specialAnonUrl) {
        this.specialAnonUrl = specialAnonUrl;
    }

    public void setEnableAnonSession(boolean enableAnonSession) {
        this.enableAnonSession = enableAnonSession;
    }

    /**
     * Creates a new instance.
     *
     * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null.
     */
    public AppSessionRepositoryFilter(AppSessionRepository sessionRepository) {
        super(sessionRepository);
        this.sessionRepository = sessionRepository;
    }

    public void setServletContextNew(ServletContext servletContext) {
        setServletContext(servletContext);
        this.servletContext = servletContext;
    }
    public void setHttpSessionIdResolverNew(HttpSessionIdResolver httpSessionIdResolver) {
        setHttpSessionIdResolver(httpSessionIdResolver);
        this.httpSessionIdResolver = httpSessionIdResolver;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//        super.doFilterInternal(request, response, filterChain);
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

        AppSessionRepositoryFilter.SessionRepositoryRequestWrapper wrappedRequest = new AppSessionRepositoryFilter.SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        }
        finally {
            wrappedRequest.commitSession();
        }
    }

    private final class SessionRepositoryResponseWrapper
            extends OnCommittedResponseWrapper {

        private final AppSessionRepositoryFilter.SessionRepositoryRequestWrapper request;

        /**
         * Create a new {@link AppSessionRepositoryFilter.SessionRepositoryResponseWrapper}.
         * @param request the request to be wrapped
         * @param response the response to be wrapped
         */
        SessionRepositoryResponseWrapper(AppSessionRepositoryFilter.SessionRepositoryRequestWrapper request,
                                         HttpServletResponse response) {
            super(response);
            if (request == null) {
                throw new IllegalArgumentException("request cannot be null");
            }
            this.request = request;
        }

        @Override
        protected void onResponseCommitted() {
            this.request.commitSession();
        }
    }

    /**
     * A {@link HttpServletRequest} that retrieves the
     * {@link HttpSession} using a
     * {@link AppSessionRepository}.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class SessionRepositoryRequestWrapper
            extends HttpServletRequestWrapper {

        private final HttpServletResponse response;

        private final ServletContext servletContext;

        private S requestedSession;

        private boolean requestedSessionCached;

        private Boolean requestedSessionIdValid;

        private boolean requestedSessionInvalidated;

        private SessionRepositoryRequestWrapper(HttpServletRequest request,
                                                HttpServletResponse response, ServletContext servletContext) {
            super(request);
            this.response = response;
            this.servletContext = servletContext;
        }

        /**
         * Uses the {@link HttpSessionIdResolver} to write the session id to the response
         * and persist the Session.
         */
        private void commitSession() {
            if (enableAnonSession && isAnonRequestUrl(this.getRequestURI())){
                return ;
            }
            AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = getCurrentSession();
            if (wrappedSession == null) {
                if (isInvalidateClientSession()) {
                    AppSessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
                            this.response);
                }
            }
            else {
                S session = (S) wrappedSession.getSession();
                clearRequestedSessionCache();
                AppSessionRepositoryFilter.this.sessionRepository.save(session);
                String sessionId = session.getId();
                if (!isRequestedSessionIdValid()
                        || !sessionId.equals(getRequestedSessionId())) {
                    AppSessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
                            this.response, sessionId);
                }
            }
        }

        @SuppressWarnings("unchecked")
        private AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper getCurrentSession() {
            return (AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
        }

        private void setCurrentSession(AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession) {
            if (currentSession == null) {
                removeAttribute(CURRENT_SESSION_ATTR);
            }
            else {
                setAttribute(CURRENT_SESSION_ATTR, currentSession);
            }
        }

        @Override
        @SuppressWarnings("unused")
        public String changeSessionId() {
            HttpSession session = getSession(false);

            if (session == null) {
                throw new IllegalStateException(
                        "Cannot change session ID. There is no session associated with this request.");
            }

            return getCurrentSession().getSession().changeSessionId();//.changeSessionId();
        }

        @Override
        public boolean isRequestedSessionIdValid() {
            if (this.requestedSessionIdValid == null) {
                S requestedSession = getRequestedSession();
                if (requestedSession != null) {
                    requestedSession.setLastAccessedTime(Instant.now());
                }
                return isRequestedSessionIdValid(requestedSession);
            }

            return this.requestedSessionIdValid;
        }

        private boolean isRequestedSessionIdValid(S session) {
            if (this.requestedSessionIdValid == null) {
                this.requestedSessionIdValid = session != null;
            }
            return this.requestedSessionIdValid;
        }

        private boolean isInvalidateClientSession() {
            return getCurrentSession() == null && this.requestedSessionInvalidated;
        }

        @Override
        public AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
            AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = getCurrentSession();
            if (currentSession != null) {
                return currentSession;
            }
            S requestedSession = getRequestedSession();
            if (requestedSession != null) {
                if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
                    requestedSession.setLastAccessedTime(Instant.now());
                    this.requestedSessionIdValid = true;
                    currentSession = new AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, getServletContext());
                    currentSession.setNew(false);
                    setCurrentSession(currentSession);
                    return currentSession;
                }
            }
            else {
                // This is an invalid session id. No need to ask again if
                // request.getSession is invoked for the duration of this request
//                if (SESSION_LOGGER.isDebugEnabled()) {
//                    SESSION_LOGGER.debug(
//                            "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
//                }
                setAttribute(INVALID_SESSION_ID_ATTR, "true");
            }
            if (!create) {
                return null;
            }
//            if (SESSION_LOGGER.isDebugEnabled()) {
//                SESSION_LOGGER.debug(
//                        "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
//                                + SESSION_LOGGER_NAME,
//                        new RuntimeException(
//                                "For debugging purposes only (not an error)"));
//            }

            //获取request
            ServletRequest servletRequest = AppServletContextHolder.getServletRequest();
            //生成默认sessionId
            String sessionId=UUID.randomUUID().toString();
            if(servletRequest!=null){
                //判断缓存中是否存在了用户id，如果存在，根据用户id生成sessionId
                Object user = servletRequest.getAttribute(AppSessionConstants.SESSION_LOGIN_KEY);
                if( user != null ){
                    //获取request暂存的用户Id
                    String userId=user.toString();
                    //根据用户Id生成sessionId
                    sessionId = UtilAESCrypto.encryptSessionId(userId,System.currentTimeMillis()+(Long.valueOf(appProperties.getSession().getTimeout())*1000),appProperties.getName(),appProperties.getVersion());
                }
            }

            //根据sessionId创建session
            S session = AppSessionRepositoryFilter.this.sessionRepository.createSession(sessionId);
            session.setLastAccessedTime(Instant.now());
            currentSession = new AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, getServletContext());
            setCurrentSession(currentSession);
            return currentSession;
        }




        @Override
        public ServletContext getServletContext() {
            if (this.servletContext != null) {
                return this.servletContext;
            }
            // Servlet 3.0+
            return super.getServletContext();
        }

        @Override
        public AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession() {
            return getSession(true);
        }

        @Override
        public String getRequestedSessionId() {
            S requestedSession = getRequestedSession();
            return (requestedSession != null) ? requestedSession.getId() : null;
        }

        private boolean isAnonRequestUrl(String requestUrl){
            String MATCH_ALL = "/**";

            if (UtilCollection.isNotEmpty(specialAnonUrl)){
                for (String specialUrl : specialAnonUrl) {
                    if (MATCH_ALL.equals(specialUrl) || "**".equals(specialUrl)||
                            (specialUrl.endsWith(MATCH_ALL) && requestUrl.startsWith(specialUrl.replace(MATCH_ALL,""))) ||
                            requestUrl.equals(specialUrl)) {
                        //若其中有匹配所有的URL，则直接返回false
                        return false;
                    }
                }
            }

            if (UtilCollection.isNotEmpty(anonUrlList)){
                for (String anonUrl : anonUrlList){
                    if (MATCH_ALL.equals(anonUrl) || "**".equals(anonUrl) ||
                            (anonUrl.endsWith(MATCH_ALL) && requestUrl.startsWith(anonUrl.replace(MATCH_ALL,""))) ||
                            requestUrl.equals(anonUrl)) {
                        return true;
                    }
                }
            }

            return false;
        }

        private S getRequestedSession() {

            if (enableAnonSession && isAnonRequestUrl(this.getRequestURI())){

                return this.requestedSession;
            }
            if (!this.requestedSessionCached) {
                List<String> sessionIds = AppSessionRepositoryFilter.this.httpSessionIdResolver
                        .resolveSessionIds(this);
                for (String sessionId : sessionIds) {
                    S session = AppSessionRepositoryFilter.this.sessionRepository
                            .findById(sessionId);
                    if (session != null) {
                        this.requestedSession = session;
                        break;
                    }
                }
                this.requestedSessionCached = true;
            }
            return this.requestedSession;
        }

        private void clearRequestedSessionCache() {
            this.requestedSessionCached = false;
            this.requestedSession = null;
        }

        /**
         * Allows creating an HttpSession from a Session instance.
         *
         * @author Rob Winch
         * @since 1.0
         */
        private final class HttpSessionWrapper extends HttpSessionAdapter<S> {

            HttpSessionWrapper(S session, ServletContext servletContext) {
                super(session, servletContext);
            }

            @Override
            public void invalidate() {
                super.invalidate();
                AppSessionRepositoryFilter.SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                setCurrentSession(null);
                clearRequestedSessionCache();
                AppSessionRepositoryFilter.this.sessionRepository.deleteById(getId());
            }
        }



    }
}
